/*
 * Project: Set Sample
 * Copyright (C) 2013 alf.labs gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.alflabs.setsample.model;

import com.alflabs.annotations.NonNull;
import com.alflabs.annotations.Null;

import java.util.Random;

/**
 * A set cell or card.
 *
 * Each cell has 4 dimensions: color, shape, number, pattern.
 * Each dimension has 3 choices.
 *
 * A "set" is when 3 cells are either all equal or all different in each given dimension.
 */
public final class SetCard implements Comparable<SetCard> {

    /**
     * Coding: each dimension has 3 natural values, [0, 1, 2] -- this is called the "natural value".
     * Encode this has a bit mask: 0111 in binary -- this is called the "bit-coded value".
     * To know if 3 set cells all different:
     * 0001 + 0010 + 0100 = 0111
     * When they are the same, e.g: 0001+0001+0001=0011, etc.
     * So to check a set, we get 3 ints, sum it and compare with 4 possible values
     * (sum of bit 1, sum of bit 2, sum of bit 3, or all bits set.)
     * (Alternatively we could make a lookup table, since there are only 3*3 possibilities.)
     *
     * This means each dimension naturally fits in a nibble so the 4 dimensions fit in 16 bits.
     */

    public static final int DIM_COLOR   =  0;
    public static final int DIM_SHAPE   =  1;
    public static final int DIM_PATTERN =  2;
    public static final int DIM_NUMBER  =  3;
    public static final int NUM_DIM =  4;

    private static final int V1 = 0x001;
    private static final int V2 = 0x002;
    private static final int V3 = 0x004;
    public static final int NUM_VALUES = 3;

    public static final int CARD_SPACE_SIZE = NUM_VALUES * NUM_VALUES * NUM_VALUES * NUM_VALUES;

    private static final int SUM_SAME_1 = 3*V1;
    private static final int SUM_SAME_2 = 3*V2;
    private static final int SUM_SAME_3 = 3*V3;
    private static final int SUM_DIFF   = V1+V2+V3;

    private static final int MASK = V1+V2+V3;

    static final int SHIFT_COLOR   =  0;
    static final int SHIFT_SHAPE   =  4;
    static final int SHIFT_PATTERN =  8;
    static final int SHIFT_NUMBER  = 12;

    public static final int COLOR_RED    = V1;
    public static final int COLOR_PURPLE = V2;
    public static final int COLOR_GREEN  = V3;

    public static final int SHAPE_OVAL     = V1;
    public static final int SHAPE_SQUIGGLE = V2;
    public static final int SHAPE_DIAMOND  = V3;

    public static final int NUMBER_1 = V1;
    public static final int NUMBER_2 = V2;
    public static final int NUMBER_3 = V3;

    public static final int PATTERN_SOLID    = V1;
    public static final int PATTERN_STRIPPED = V2;
    public static final int PATTERN_OUTLINE  = V3;

    public static final int RGB_COLOR_RED    = 0xFFCC0000;
    public static final int RGB_COLOR_PURPLE = 0xFF551177;
    public static final int RGB_COLOR_GREEN  = 0xFF00CC00;

    private int mValue;

    public SetCard() {
    }

    public SetCard(@NonNull SetCard copy) {
        mValue = copy.mValue;
    }

    public SetCard(int hashCode) {
        mValue = hashCode;
    }

    /**
     * Creates a new card with the specified attributes using bit values (V1, V2 or V3)
     *
     * @param colorV   A bit value COLOR_x   (V1, V2 or V3)
     * @param shapeV   A bit value SHAPE_x   (V1, V2 or V3)
     * @param numberV  A bit value NUMBER_x  (V1, V2 or V3)
     * @param patternV A bit value PATTERN_x (V1, V2 or V3)
     */
    public SetCard(int colorV, int shapeV, int numberV, int patternV) {
        setDimensionV(DIM_COLOR,   colorV);
        setDimensionV(DIM_SHAPE,   shapeV);
        setDimensionV(DIM_NUMBER,  numberV);
        setDimensionV(DIM_PATTERN, patternV);
    }

    /**
     * Clear the value of all dimensions. No bits are set. This is not a valid cell,
     * it must be initialized after.
     */
    @NonNull
    public SetCard clear() {
        mValue = 0;
        return this;
    }

    /**
     * Randomize the dimensions values.
     * @param rnd A non-null random generator, already seeded.
     */
    @NonNull
    public SetCard randomize(@NonNull Random rnd) {
        setDimension012(DIM_COLOR, rnd.nextInt(3));
        setDimension012(DIM_SHAPE, rnd.nextInt(3));
        setDimension012(DIM_NUMBER, rnd.nextInt(3));
        setDimension012(DIM_PATTERN, rnd.nextInt(3));
        return this;
    }

    /**
     * Initialize using another card. Basically makes a copy.
     * @param copy The instance of copy.
     * @return This instance for chaining.
     */
    @NonNull
    public SetCard set(@NonNull SetCard copy) {
        mValue = copy.mValue;
        return this;
    }

    /**
     * Sets a specific dimension bit value (V1, V2 or V3)
     *
     * @param dimensionIndex One of the DIM_xyz constants, indicating the dimension to set.
     * @param bitValue123 A bit value (V1, V2 or V3)
     */
    @NonNull
    public SetCard setDimensionV(int dimensionIndex, int bitValue123) {
        setV(dimension2Shift(dimensionIndex), bitValue123);
        return this;
    }

    /**
     * Sets a specific dimension natural value (0, 1 or 2)
     *
     * @param dimensionIndex One of the DIM_xyz constants, indicating the dimension to set.
     * @param naturalValue012 A natural value of 0, 1 or 2 for the given dimension.
     *                        (for {@link #DIM_NUMBER}, [0,1,2] maps to [1,2,3].)
     */
    @NonNull
    public SetCard setDimension012(int dimensionIndex, int naturalValue012) {
        setV(dimension2Shift(dimensionIndex), nat2V(naturalValue012));
        return this;
    }

    @Override
    public int compareTo(@NonNull SetCard another) {
        return mValue - another.mValue;
    }

    @Override
    public boolean equals(@Null Object o) {
        if (this == o) return true;
        //noinspection SimplifiableIfStatement
        if (o instanceof SetCard) {
            return mValue == ((SetCard) o).mValue;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return mValue;
    }

    @SuppressWarnings("PointlessBitwiseExpression")
    public int hashCodeColorPattern() {
        return mValue & ((MASK << SHIFT_COLOR) | (MASK << SHIFT_PATTERN));
    }

    public int getColor() {
        return _extract(SHIFT_COLOR, mValue);
    }

    public int getShape() {
        return _extract(SHIFT_SHAPE, mValue);
    }

    public int getPattern() {
        return _extract(SHIFT_PATTERN, mValue);
    }

    public int getNumber() {
        return _extract(SHIFT_NUMBER, mValue);
    }

    @Override
    @NonNull
    public String toString() {
        StringBuilder sb = new StringBuilder();

        switch(_extract(SHIFT_NUMBER, mValue)) {
        case NUMBER_1:  sb.append("1x "); break;
        case NUMBER_2:  sb.append("2x "); break;
        case NUMBER_3:  sb.append("3x "); break;
        }

        //noinspection PointlessBitwiseExpression
        switch(_extract(SHIFT_COLOR, mValue)) {
        case COLOR_RED:     sb.append("Red "); break;
        case COLOR_PURPLE:  sb.append("Purple "); break;
        case COLOR_GREEN:   sb.append("Green "); break;
        }

        switch(_extract(SHIFT_SHAPE, mValue)) {
        case SHAPE_OVAL:    sb.append("Oval "); break;
        case SHAPE_SQUIGGLE:    sb.append("Squiggle "); break;
        case SHAPE_DIAMOND:     sb.append("Diamond "); break;
        }

        switch(_extract(SHIFT_PATTERN, mValue)) {
        case PATTERN_SOLID:     sb.append("Solid"); break;
        case PATTERN_STRIPPED:  sb.append("Stripped"); break;
        case PATTERN_OUTLINE:   sb.append("Outline"); break;
        }

        return sb.toString();
    }

    /** True if all 3 cells are a set: each dimension is either equal or all different. */
    public static boolean isSet(@NonNull SetCard c1, @NonNull SetCard c2, @NonNull SetCard c3) {
        int i1 = c1.mValue;
        int i2 = c2.mValue;
        int i3 = c3.mValue;

        return _isAllSameOrDiff(_extract(SHIFT_COLOR  , i1), _extract(SHIFT_COLOR  , i2), _extract(SHIFT_COLOR  , i3))
            && _isAllSameOrDiff(_extract(SHIFT_SHAPE  , i1), _extract(SHIFT_SHAPE  , i2), _extract(SHIFT_SHAPE  , i3))
            && _isAllSameOrDiff(_extract(SHIFT_NUMBER , i1), _extract(SHIFT_NUMBER , i2), _extract(SHIFT_NUMBER , i3))
            && _isAllSameOrDiff(_extract(SHIFT_PATTERN, i1), _extract(SHIFT_PATTERN, i2), _extract(SHIFT_PATTERN, i3));
    }

    /** Describes a Set solution: which dimensions are equal or different. */
    @NonNull
    public static String describeSet(@NonNull SetCard c1, @NonNull SetCard c2, @NonNull SetCard c3) {
        int i1 = c1.mValue;
        int i2 = c2.mValue;
        int i3 = c3.mValue;

        StringBuilder sb = new StringBuilder();

        if (       _isAllSame(_extract(SHIFT_COLOR  , i1), _extract(SHIFT_COLOR  , i2), _extract(SHIFT_COLOR  , i3))) {
            sb.append("Same colors, ");
        } else if (_isAllDiff(_extract(SHIFT_COLOR  , i1), _extract(SHIFT_COLOR  , i2), _extract(SHIFT_COLOR  , i3))) {
            sb.append("Different colors, ");
        }

        if (       _isAllSame(_extract(SHIFT_SHAPE  , i1), _extract(SHIFT_SHAPE  , i2), _extract(SHIFT_SHAPE  , i3))) {
            sb.append("Same shapes, ");
        } else if (_isAllDiff(_extract(SHIFT_SHAPE  , i1), _extract(SHIFT_SHAPE  , i2), _extract(SHIFT_SHAPE  , i3))) {
            sb.append("Different shapes, ");
        }

        if (       _isAllSame(_extract(SHIFT_NUMBER , i1), _extract(SHIFT_NUMBER , i2), _extract(SHIFT_NUMBER , i3))) {
            sb.append("Same numbers, ");
        } else if (_isAllDiff(_extract(SHIFT_NUMBER, i1), _extract(SHIFT_NUMBER, i2), _extract(SHIFT_NUMBER, i3))) {
            sb.append("Different numbers, ");
        }

        if (       _isAllSame(_extract(SHIFT_PATTERN, i1), _extract(SHIFT_PATTERN, i2), _extract(SHIFT_PATTERN, i3))) {
            sb.append("Same patterns, ");
        } else if (_isAllDiff(_extract(SHIFT_PATTERN, i1), _extract(SHIFT_PATTERN, i2), _extract(SHIFT_PATTERN, i3))) {
            sb.append("Different patterns, ");
        }

        int n = sb.length();
        if (n > 2) {
            sb.delete(n-2, n);
            sb.append('.');
        }

        return sb.toString();
    }

    /**
     * Converts from a dimension index to a dimension shift
     * @param dimensionIndex One of the DIM_xyz constants.
     * @return One of the SHIFT_xyz constants.
     */
    private int dimension2Shift(int dimensionIndex) {
        switch (dimensionIndex) {
        case DIM_COLOR  : return SHIFT_COLOR;
        case DIM_SHAPE  : return SHIFT_SHAPE;
        case DIM_NUMBER : return SHIFT_NUMBER;
        default:
        case DIM_PATTERN: return SHIFT_PATTERN;
        }
    }

    /**
     * Sets/replaces the dimension value into mValue.
     *
     * @param dimensionShift The dimension's shift.
     * @param v123 The bit-coded V1, V2 or V3 value for the dimension.
     */
    private void setV(int dimensionShift, int v123) {
        mValue = (mValue & ~(MASK << dimensionShift)) | (v123 << dimensionShift);
    }

    /**
     * Extracts the bit-coded value of a dimension:
     * get the V1, V2, V3 bit-coded value out of the int value at the given dimension shift.
     *
     * @param value The int mValue of the cell.
     * @param dimensionShift One of the shift constants.
     * @return One of V1, V2 or V3.
     */
    private static int _extract(int dimensionShift, int value) {
        return (value >> dimensionShift) & MASK;
    }

    /**
     * Transforms one dimension from its natural [0, 1, 2] value into the bit-coded V1, V2 or V3.
     *
     * @param v012 Natural value, either 0, 1 or 2.
     * @return Bit-coded value, one of V1, V2 or V3.
     */
    private static int nat2V(int v012) {
        switch(v012) {
        default:
        case 0: return V1;
        case 1: return V2;
        case 2: return V3;
        }
    }

    /**
     * Indicates whether the given dimensions are either all the same or all different.
     *
     * @param v1 The _extract'ed bit-coded value dimension of cell 1
     * @param v2 The _extract'ed bit-coded value dimension of cell 2
     * @param v3 The _extract'ed bit-coded value dimension of cell 3
     * @return True if the 3 values are all the same or all different.
     */
    private static boolean _isAllSameOrDiff(int v1, int v2, int v3) {
        if (v1 == v2 && v2 == v3) {
            return true;
        } else if (v1 != v2 && v2 != v3 && v1 != v3) {
            return true;
        }
        return false;
    }

    /**
     * Indicates whether the given dimensions are either all the same.
     *
     * @param v1 The _extract'ed bit-coded value dimension of cell 1
     * @param v2 The _extract'ed bit-coded value dimension of cell 2
     * @param v3 The _extract'ed bit-coded value dimension of cell 3
     * @return True if the 3 values are all the same or all different.
     */
    private static boolean _isAllSame(int v1, int v2, int v3) {
        return v1 == v2 && v2 == v3;
    }

    /**
     * Indicates whether the given dimensions are either all different.
     *
     * @param v1 The _extract'ed bit-coded value dimension of cell 1
     * @param v2 The _extract'ed bit-coded value dimension of cell 2
     * @param v3 The _extract'ed bit-coded value dimension of cell 3
     * @return True if the 3 values are all the same or all different.
     */
    private static boolean _isAllDiff(int v1, int v2, int v3) {
        return v1 != v2 && v2 != v3 && v1 != v3;
    }
}
